W4. Statements, Expressions, Structures, and Unions in C

Author

Eugene Zouev, Munir Makhmutov

Published

September 23, 2025

Quiz | Flashcards

1. Summary

1.1 Statements in C
1.1.1 What is a Statement?

In C, a statement is a complete instruction that tells the computer to perform an action. Every statement in C ends with a semicolon (;). The general rule is that statements do not produce values; their purpose is to control the flow of execution or to cause a side effect, such as modifying a variable’s value.

1.1.2 Categories of Statements

C statements can be grouped into several categories:

  • Selection Statements: These statements choose which code path to execute based on a condition.
    • if-else: Executes a block of code if a condition is true, and an optional else block if it is false.
    • switch: Evaluates an integer expression and jumps to the case that matches the value. A break statement is typically used to exit the switch; otherwise, execution “falls through” to the next case.
  • Iteration Statements (Loops): These statements execute a block of code repeatedly as long as a condition is met.
    • while: Evaluates the condition before each execution of the loop body.
    • do-while: Executes the loop body at least once and then evaluates the condition after each execution.
    • for: A compact loop structure that combines initialization, condition checking, and a post-iteration action in one line.
  • Jump Statements: These statements unconditionally transfer control to another part of the program.
    • break: Exits the innermost loop or switch statement.
    • continue: Skips the remainder of the current loop iteration and proceeds to the next one.
    • return: Exits the current function and optionally returns a value to the caller.
    • goto: Transfers control to a labeled statement within the same function. Its use is generally discouraged as it can make code difficult to read and debug.
  • Compound Statements (Blocks): A group of zero or more statements enclosed in curly braces ({}). A block can be used anywhere a single statement is expected, allowing you to group multiple instructions. A declaration within a block is considered a statement.
  • Special Statements:
    • Expression Statement: An expression followed by a semicolon. The expression is evaluated, and its result is discarded. Its main purpose is for the side effect it produces (e.g., assignment a = b + c; or a function call printf("hello");).
    • Declaration Statement: A statement that introduces a new variable (e.g., int x = 10;). In modern C, declarations can appear anywhere a statement is allowed, not just at the beginning of a block.
1.2 Expressions in C
1.2.1 What is an Expression?

An expression is a formula—a combination of variables, constants, operators, and function calls—that is evaluated to produce a single value. Almost every expression in C produces a value.

1.2.2 Building Blocks of Expressions

Expressions are constructed from various elements, ordered by complexity:

  1. Primary Expressions: The most basic elements.
    • Identifier: The name of a variable or function.
    • Literal: A constant value (e.g., 123, 0.01E-2, "string").
    • Parenthesized expression: An expression enclosed in parentheses, like (a + b).
  2. Postfix Expressions: Built on primary expressions.
    • Array subscripting: arr[i+j].
    • Function call: func(*p, 777).
    • Member access: s.m (for struct/union) or ptr->m (for pointer to struct/union).
    • Postfix increment/decrement: x++, x--.
  3. Unary Expressions: Operators that act on a single operand.
    • Prefix increment/decrement: ++x, --x.
    • Address-of (&) and indirection (*): &x, *p.
    • Unary plus/minus: +x, -x.
    • Logical NOT (!) and bitwise NOT (~).
    • sizeof: An operator that returns the size, in bytes, of a type or variable.
  4. Binary Expressions: Operators that act on two operands (e.g., a + b, c * d).
  5. Ternary Expression: The conditional operator (? :), which takes three operands (condition ? value_if_true : value_if_false).
1.2.3 Operator Precedence and Associativity
  • Precedence: Determines the order in which operators are evaluated in a complex expression. For example, * and / have higher precedence than + and -, so a + b * c is evaluated as a + (b * c).
  • Associativity: Determines the order for operators of the same precedence. Most binary operators are left-to-right associative (e.g., x - y + z is (x - y) + z). Unary operators and the assignment operator are right-to-left associative (e.g., x = y = 5 is x = (y = 5)).
1.2.4 Side Effects

A side effect is any change in the state of the program, such as modifying a variable or performing I/O. Expressions like x++ are valued for their side effect. The expression x++ evaluates to the original value of x, and as a side effect, x is incremented.

1.3 Recursive Functions

A recursive function is a function that calls itself to solve a problem. This is effective for problems that can be divided into smaller, self-similar subproblems.

1.3.1 Core Components of Recursion

To prevent infinite execution, a recursive function must have two parts:

  • Base Case: A simple condition where the function does not call itself and returns a direct answer. This is the stopping point.
  • Recursive Step: The part of the function that calls itself, but with a modified argument that brings it closer to the base case.
1.3.2 The Call Stack in Recursion

When a function is called, a frame containing its local variables is pushed onto the call stack. In recursion, a new frame is added for each self-call. The stack grows until a base case is reached. Then, as each function returns its result to its caller, its frame is popped off the stack, and the stack unwinds.

1.4 Structures (struct)

A structure is a user-defined data type that groups related variables of different types into a single logical unit.

1.4.1 Declaration and Memory

Each member of a struct is stored in its own unique memory location. The total size is the sum of the sizes of its members plus any memory padding.

  • Memory Padding: Compilers insert unused bytes between members to align them on memory addresses that are optimal for the hardware (e.g., a 4-byte int on an address divisible by 4). This speeds up access but increases the structure’s size.
  • Packed Structures: A non-standard feature (__attribute__((packed))) that removes padding. This saves memory but can cause slower performance or crashes on some architectures.
1.4.2 Accessing Members
  • Dot Operator (.): Used to access members of a structure variable directly (e.g., student.id).
  • Arrow Operator (->): Used to access members via a pointer to a structure (e.g., studentPtr->id). This is equivalent to (*studentPtr).id.
1.5 Unions (union)

A union is a special data type where all members share the same memory location.

1.5.1 Shared Memory

A union is only allocated enough memory to hold its largest member. When you assign a value to one member, it may alter the data of any other member, because they all occupy the same bytes. This is useful for saving memory or for interpreting the same data in multiple ways (type punning). You can only meaningfully use one member at a time.


2. Definitions

  • Statement: A complete instruction in a C program, ending with a semicolon.
  • Expression: A combination of values, variables, operators, and functions that evaluates to a single value.
  • Expression Statement: An expression followed by a semicolon, executed for its side effects.
  • Declaration Statement: A statement that declares and optionally initializes a new variable.
  • Operator Precedence: The rules that define the order in which different operators are evaluated in an expression.
  • Side Effect: An action by a function or expression that modifies state, such as changing a variable’s value or performing I/O.
  • Recursion: A technique where a function calls itself to solve a problem.
  • Base Case: The condition in a recursive function that terminates the recursion.
  • Structure (struct): A data type that groups variables of different types, with each member stored in a separate memory location.
  • Union (union): A data type where all members share the same memory location.
  • Memory Padding: Empty bytes inserted by a compiler into a structure to align its members for more efficient memory access.
  • Typedef: A keyword used to create an alias for a data type, often used to simplify struct and union declarations.
  • Call Stack: A data structure that stores information about the active subroutines of a computer program.

3. Examples

3.1. Analyze Program Output with Static Variables and Recursion (Lab 4, Task 1)

What is the expected output of this program?

#include <stdio.h>

void func() {
    static int x = 5;
    int y = 5;
    while (y < 10 && x < 10) {
        printf("x = %d, y = %d\n", x, y);
        x++;
        y++;
        func();
    }
}

int main() {
    func();
}
Click to see the solution

Let’s trace the execution step-by-step.

  1. main calls func().
    • func (Call 1):
      • static int x = 5; This line only runs once for the entire program’s lifetime. x is created and set to 5.
      • int y = 5; y is a local variable, created and set to 5 for this call.
      • while loop starts. Condition (y < 10 && x < 10) is (5 < 10 && 5 < 10), which is true.
      • printf prints: x = 5, y = 5
      • x becomes 6. (The static x is now 6).
      • y becomes 6. (The local y is now 6).
      • func() is called recursively.
  2. func (Call 2):
    • The static int x = 5; line is skipped. x retains its value of 6.
    • int y = 5; A new local y is created and set to 5.
    • while loop starts. Condition (y < 10 && x < 10) is (5 < 10 && 6 < 10), which is true.
    • printf prints: x = 6, y = 5
    • x becomes 7.
    • y becomes 6.
    • func() is called recursively.
  3. func (Call 3):
    • x is 7. New local y is 5.
    • while is (5 < 10 && 7 < 10), which is true.
    • printf prints: x = 7, y = 5
    • x becomes 8. y becomes 6.
    • func() is called recursively.
  4. func (Call 4):
    • x is 8. New local y is 5.
    • while is (5 < 10 && 8 < 10), which is true.
    • printf prints: x = 8, y = 5
    • x becomes 9. y becomes 6.
    • func() is called recursively.
  5. func (Call 5):
    • x is 9. New local y is 5.
    • while is (5 < 10 && 9 < 10), which is true.
    • printf prints: x = 9, y = 5
    • x becomes 10. y becomes 6.
    • func() is called recursively.
  6. func (Call 6):
    • x is 10. New local y is 5.
    • while condition (y < 10 && x < 10) is (5 < 10 && 10 < 10), which is false.
    • The loop is skipped. The function func (Call 6) returns.
  7. Execution returns to func (Call 5).
    • The while loop in Call 5 continues. Its local y was 6. The static x is now 10.
    • while condition (y < 10 && x < 10) is (6 < 10 && 10 < 10), which is false.
    • Loop terminates. func (Call 5) returns.
  8. This pattern continues. The program unwinds from the recursion, but the while loop condition x < 10 is now always false for every pending call. All func calls return without printing anything further.

Final Output:

x = 5, y = 5
x = 6, y = 5
x = 7, y = 5
x = 8, y = 5
x = 9, y = 5
3.2. Student and Exam Day Structures (Lab 4, Task 2)

Write a program that will contain two structures: student and exam_day. The first structure should contain information about the student’s name, surname, group number, and a variable for the second structure. The second structure should contain the day, year, and month of the exam. The month has to be in letter representation (For example, May), not numbers. The program should require the user to enter all the fields and then print them.

Click to see the solution
#include <stdio.h>
#include <string.h>

// Define the structure for the exam date.
struct exam_day {
    int day;
    char month[20]; // Character array to store the month's name
    int year;
};

// Define the structure for the student.
// This structure contains another structure as one of its members.
struct student {
    char name[50];
    char surname[50];
    int group_number;
    struct exam_day exam_date; // Nested structure
};

int main() {
    // Declare a variable of type 'student'.
    struct student s1;

    // --- Get User Input ---
    printf("Enter student's first name: ");
    scanf("%s", s1.name);

    printf("Enter student's surname: ");
    scanf("%s", s1.surname);

    printf("Enter student's group number: ");
    scanf("%d", &s1.group_number);

    printf("Enter exam day (e.g., 22): ");
    scanf("%d", &s1.exam_date.day);

    printf("Enter exam month (e.g., October): ");
    scanf("%s", s1.exam_date.month);

    printf("Enter exam year (e.g., 2025): ");
    scanf("%d", &s1.exam_date.year);

    // --- Print the Stored Information ---
    printf("\n--- Student Information ---\n");
    printf("Name: %s\n", s1.name);
    printf("Surname: %s\n", s1.surname);
    printf("Group: %d\n", s1.group_number);
    printf("Exam Date: %d %s %d\n", s1.exam_date.day, s1.exam_date.month, s1.exam_date.year);

    return 0;
}
3.3. Encrypt an Integer using a Union (Lab 4, Task 3)

Using a union, write a program that will read an unsigned long long integer via the console and then encrypt it by swapping the values of each odd byte and its neighbor even byte, beginning with the most significant byte. The program must contain an encryption(...) function and print the original, encrypted, and decrypted messages.

Click to see the solution
#include <stdio.h>

// A union allows storing different data types in the same memory location.
// Here, we can access the same 8 bytes of memory as either a single
// unsigned long long or as an array of 8 individual bytes (unsigned chars).
typedef union {
    unsigned long long ull_value;
    unsigned char bytes[8];
} ull_converter;

// Function to perform the byte-swapping encryption/decryption.
// The same logic works for both encryption and decryption.
void encryption(ull_converter *data) {
    // An unsigned long long is 8 bytes. We swap pairs of bytes:
    // bytes[0] with bytes[1]
    // bytes[2] with bytes[3]
    // bytes[4] with bytes[5]
    // bytes[6] with bytes[7]
    // The loop iterates 4 times for the 4 pairs.
    for (int i = 0; i < 8; i += 2) {
        // Use a temporary variable to swap the byte pair.
        unsigned char temp = data->bytes[i];
        data->bytes[i] = data->bytes[i+1];
        data->bytes[i+1] = temp;
    }
}

int main() {
    // Create a union variable.
    ull_converter data;

    // Prompt user for input.
    printf("Enter an unsigned long long integer: ");
    // Read the value from the user. %llu is the format specifier for unsigned long long.
    scanf("%llu", &data.ull_value);

    // Print the original value.
    printf("Original message: %llu\n", data.ull_value);

    // Call the encryption function. We pass the address of the union.
    encryption(&data);

    // The bytes inside the union have been swapped. Reading the ull_value now gives the encrypted number.
    printf("Encrypted message: %llu\n", data.ull_value);

    // Call the function again. Swapping the swapped bytes returns them to their original positions.
    encryption(&data);

    // Print the decrypted message, which should match the original.
    printf("Decrypted message: %llu\n", data.ull_value);

    return 0;
}